package app;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JOptionPane;

import app.gui.IMyListener;
import app.object.CloudPoint;
import app.object.GeoidGeoPoint;
import app.object.GeoidUTMPoint;

public class Manager {

	private static Manager instance = null;

	private String loadedCloudFilePath;

	final int radius = 100;

	private int cloudSize = 0;

	private Set<IMyListener> listeners = new HashSet<IMyListener>();

	public int getCloudSize() {
		return cloudSize;
	}

	public void setCloudSize(int cloudSize) {
		this.cloudSize = cloudSize;
	}

	public String getLoadedCloudFilePath() {
		return loadedCloudFilePath;
	}

	public void setLoadedCloudFilePath(String loadedCloudFilePath) {
		this.loadedCloudFilePath = loadedCloudFilePath;
	}

	private int stripe;

	private String targetFilePath;

	private DecimalFormat f = new DecimalFormat("0.000");

	public int getStripe() {
		return stripe;
	}

	public String getTargetFilePath() {
		return targetFilePath;
	}

	public void setTargetFilePath(String targetFilePath) {
		this.targetFilePath = targetFilePath;
	}

	public void setStripe(int stripe) {
		this.stripe = stripe;
	}

	public synchronized static Manager getInstance() {

		if (instance == null) {
			instance = new Manager();
		}
		return instance;
	}

	/**
	 * @param row
	 */
	public void startTransformation(int row) {
		// calculate center of point-cloud and set the size of the
		// point-cloud;
		CloudPoint centralUTMPoint = getCenterOfPointCloud();

		// create ArrayList with all Geoid-points closer than 20km to that
		// center-point
		List<GeoidUTMPoint> geoidUTMPoints = getGeoidArray(centralUTMPoint);

		if (geoidUTMPoints.isEmpty()) {
			// if the array is empty, there must be something wrong with the
			// Geoid-file
			JOptionPane.showMessageDialog(null,
					"Es wurden keine Geoid-Punkte geladen!");
		} else {
			try {
				// file name for protocol-file and target-file;
				String fileName = getFileName();

				// target-file;
				File file = new File(fileName + "-HS160.xyz");
				BufferedWriter fileWriter = new BufferedWriter(
						new OutputStreamWriter(new FileOutputStream(file)));

				FileReader fr = new FileReader(getLoadedCloudFilePath());
				BufferedReader br = new BufferedReader(fr);

				String line;
				int cloudRowCounter = 0;

				int geoidLineCounter = 0;

				// the closest Geoid-point to the current UTM-point;
				GeoidUTMPoint closestGeoidUTMPoint = null;

				setTargetFilePath(file.getAbsolutePath());

				fileWriter.write("HS160");

				while ((line = br.readLine()) != null && !line.isEmpty()) {

					if (cloudRowCounter == 0 || cloudRowCounter % row == 0) {

						// load UTM-Point from PointCloud-File
						CloudPoint cloudPoint = getUTMPointFromLine(line);

						// find closest Point from QGeoid
						for (GeoidUTMPoint gp : geoidUTMPoints) {
							GeoidUTMPoint currentGeoidUTMPoint = gp;

							// set first geoid-point as the closest:
							if (geoidLineCounter == 0) {
								closestGeoidUTMPoint = currentGeoidUTMPoint;
							}
							geoidLineCounter++;

							// calculate distance between GeoidPoint and
							// UTM-Point
							if (geoidLineCounter > 0
									&& getDistance(cloudPoint,
											currentGeoidUTMPoint) < getDistance(
											cloudPoint, closestGeoidUTMPoint)) {
								closestGeoidUTMPoint = currentGeoidUTMPoint;
							}

						}

						fileWriter.newLine();
						fileWriter
								.write(f.format(cloudPoint.getEasting())
										+ " "
										+ f.format(cloudPoint.getNorthing())
										+ " "
										+ f.format((cloudPoint.getHeight() - closestGeoidUTMPoint
												.getHeight())));

					}
					cloudRowCounter++;

					// after every 100 points show progress on the dialog frame:
					if (cloudRowCounter % 500 == 0) {
						String string = new String("noch "
								+ (cloudSize - cloudRowCounter)
								+ " Datensaetze");
						fireListChanged(string);
					}

				}
				fileWriter.close();
				br.close();

				// protocol-file
				File protocol = new File(fileName + "-protocol.txt");
				BufferedWriter protocolWriter = new BufferedWriter(
						new OutputStreamWriter(new FileOutputStream(protocol)));

				// write date
				Calendar calendar = new GregorianCalendar();
				Format format = DateFormat.getDateTimeInstance(DateFormat.LONG,
						DateFormat.LONG);
				Date date = calendar.getTime();
				protocolWriter.write(format.format(date));
				protocolWriter.newLine();

				// write source-file path
				protocolWriter.write("Ausgangsdatei: " + loadedCloudFilePath);
				protocolWriter.newLine();

				// write Geoid-file path
				protocolWriter.write("Verwendetes Geoidmodell: "
						+ new File("resource/Geoid.txt").getAbsolutePath());
				protocolWriter.newLine();
				// write center of point cloud
				protocolWriter.write("Anzahl der Punkte in Punktwolke: "
						+ cloudSize);
				protocolWriter.newLine();
				protocolWriter.write("Berechneter Mittelpunkt: "
						+ centralUTMPoint.toString());
				protocolWriter.newLine();

				// write size of Geoid-array
				protocolWriter
						.write("Anzahl der Geoid-Punkte im Umkreis von 20km: "
								+ geoidUTMPoints.size());
				protocolWriter.newLine();

				// write target-file path
				protocolWriter.write("Pfad der Ausgabedatei: "
						+ new File(fileName + "-protocol.txt")
								.getAbsolutePath());
				protocolWriter.newLine();

				protocolWriter.close();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}

	private String getFileName() {
		String root = getLoadedCloudFilePath();
		root = getLoadedCloudFilePath().substring(
				getLoadedCloudFilePath().lastIndexOf("\\") + 1,
				getLoadedCloudFilePath().indexOf("."));
		return root;
	}

	/**
	 * @param centralUTMPoint
	 * @return List of all local Geoid-Points closer than 20km to the center of
	 *         the point cloud
	 */
	private List<GeoidUTMPoint> getGeoidArray(CloudPoint centralUTMPoint) {
		List<GeoidUTMPoint> geoidUTMPoints = new ArrayList<GeoidUTMPoint>();

		try {
			FileReader fr = new FileReader(new File("resource/Geoid.txt"));
			BufferedReader geoidBr = new BufferedReader(fr);
			String geoidLine;

			while ((geoidLine = geoidBr.readLine()) != null
					&& !geoidLine.isEmpty()) {

				GeoidUTMPoint currentGeoidUTMPoint = getLocalGeoidPointFromLine(geoidLine);

				// fill List with all points having a valid height and beeing
				// closer than 20km to the pointcloud
				if (getDistance(centralUTMPoint, currentGeoidUTMPoint) < radius
						&& currentGeoidUTMPoint.getHeight() != 999999) {
					geoidUTMPoints.add(new GeoidUTMPoint(currentGeoidUTMPoint
							.getEasting(), currentGeoidUTMPoint.getNorthing(),
							currentGeoidUTMPoint.getHeight()));
				}
			}
			geoidBr.close();

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (geoidUTMPoints.size() == 0) {

		}

		return geoidUTMPoints;
	}

	private CloudPoint getCenterOfPointCloud() {
		CloudPoint centralUTMPoint = null;
		try {
			FileReader fr = new FileReader(getLoadedCloudFilePath());
			BufferedReader br = new BufferedReader(fr);
			String line;
			int counter = 0;

			CloudPoint firstUtmPoint = null;
			CloudPoint lastUTMPoint = null;

			while ((line = br.readLine()) != null && !line.isEmpty()) {
				// get first point from UTM-File
				if (counter == 0) {
					firstUtmPoint = getUTMPointFromLine(line);
				}

				// set current UTM-Point from line to lastUTMPoint;
				lastUTMPoint = getUTMPointFromLine(line);
				counter++;
			}

			// now counter has the dimension of the point-cloud;
			setCloudSize(counter);
			br.close();
			centralUTMPoint = new CloudPoint(
					(firstUtmPoint.getEasting() + lastUTMPoint.getEasting()) / 2,
					(firstUtmPoint.getNorthing() + lastUTMPoint.getNorthing()) / 2,
					(firstUtmPoint.getHeight() + lastUTMPoint.getHeight()) / 2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// the rough center-point is the average of the first and the last point
		// in the point-cloud;
		return centralUTMPoint;
	}

	/**
	 * returns the distance between two points in km;
	 */
	private double getDistance(CloudPoint cloudPoint,
			GeoidUTMPoint geoidUTMPoint) {
		if (cloudPoint != null && geoidUTMPoint != null) {
			double d = Math.sqrt(Math.pow(cloudPoint.getEasting()
					- geoidUTMPoint.getEasting(), 2)
					+ Math.pow(
							cloudPoint.getNorthing()
									- geoidUTMPoint.getNorthing(), 2));
			return d / 1000;
		}
		return 0;
	}

	/**
	 * @param Line
	 *            from Goid-file
	 * @return local UTM-Geoid-Point
	 */
	private GeoidUTMPoint getLocalGeoidPointFromLine(String string) {
		// first extract the geographic Geoid-Point from the String-
		GeoidGeoPoint geoidGeoPoint = getGeoidGeoPointFromLine(string);

		// transform that geographic in the UTM-System:
		GeoidUTMPoint geoidUTMPoint = transformGeoToUTM(geoidGeoPoint);

		return geoidUTMPoint;
	}

	/**
	 * @param geoidGeoPoint
	 * @return UTM-Point from Geo-Point
	 */
	private GeoidUTMPoint transformGeoToUTM(GeoidGeoPoint geoidGeoPoint) {
		// Formelsammlung, Gruber - Jockel, 12.Auflage, S.114
		double b = geoidGeoPoint.getLatitude();
		double l = geoidGeoPoint.getLongitude();

		double p = 180 / Math.PI;
		double t = Math.tan(b);
		double n2 = 0.006768170 * Math.pow(Math.cos(b), 2);
		double c = 6399936.608;
		double n = c / Math.sqrt(1 + n2);

		double g0 = 111136.536655;
		double g2 = -16107.0347;
		double g4 = 16.9763;
		double g6 = -0.0223;
		double g = g0 * b + g2 * Math.sin(2 * b) + g4 * Math.sin(4 * b) + g6
				* Math.sin(6 * b);

		double m = 0.9996;

		double p1 = (m / p) * n * Math.cos(b);
		double p3 = (m / (6 * Math.pow(p, 3))) * n * Math.pow(Math.cos(b), 3)
				* (1 - Math.pow(t, 2) + n2);
		double p5 = (m / (120 * Math.pow(p, 5))) * n * Math.pow(Math.cos(b), 5)
				* (5 - 18 * Math.pow(t, 4) + n2 * (14 - 58 * Math.pow(t, 2)));
		double p2 = (m / (2 * Math.pow(p, 2))) * n * Math.pow(Math.cos(b), 2)
				* t;
		double p4 = (m / (24 * Math.pow(p, 2))) * n * Math.pow(Math.cos(b), 4)
				* t * (5 - Math.pow(t, 2) + 9 * n2);
		double p6 = (m / (720 * Math.pow(p, 6))) * n * Math.pow(Math.cos(b), 6)
				* t * (61 - 58 * Math.pow(t, 2) + Math.pow(t, 4));

		double e0 = ((getL0() + 3) / 6 + 30.5) * 1000000;
		double deltaL = l - getL0();

		double easting = e0 + p1 * deltaL + p3 * Math.pow(deltaL, 3) + p5
				* Math.pow(deltaL, 5);
		double northing = m * g + p2 * Math.pow(deltaL, 2) + p4
				* Math.pow(deltaL, 4) + p6 * Math.pow(deltaL, 6);

		return new GeoidUTMPoint(easting - e0 + 500000, northing,
				geoidGeoPoint.getHeight());
	}

	private int getL0() {
		int l0 = 9;
		if (getStripe() == 31) {
			l0 = 3;
		} else if (getStripe() == 33) {
			l0 = 15;
		}
		return l0;
	}

	private GeoidGeoPoint getGeoidGeoPointFromLine(String qgLine) {
		// latitude, longitude, height
		int lineCounter = 0;
		String latitudeString = "";
		for (int i = 0; i < qgLine.length(); i++) {
			lineCounter = i;
			if (String.valueOf(qgLine.charAt(i)).equals(" ")) {
				break;
			}
			latitudeString += String.valueOf(qgLine.charAt(i));
		}
		double latitude = Double.valueOf(latitudeString);

		String longitudeString = "";
		for (int i = 0; i < qgLine.length(); i++) {
			if (!(String.valueOf(qgLine.charAt(lineCounter)).equals(" "))) {
				break;
			}
			lineCounter++;
		}
		for (int i = 0; i < qgLine.length(); i++) {
			if (String.valueOf(qgLine.charAt(lineCounter)).equals(" ")) {
				break;
			}
			longitudeString += String.valueOf(qgLine.charAt(lineCounter));
			lineCounter++;
		}
		double longitude = Double.valueOf(longitudeString);

		double height = Double.valueOf(qgLine.substring(lineCounter + 1,
				qgLine.length()));

		return new GeoidGeoPoint(longitude, latitude, height);
	}

	private CloudPoint getUTMPointFromLine(String line) {

		int lineCounter = 0;
		String eastingString = "";

		for (int i = 0; i < line.length(); i++) {
			lineCounter = i;
			if (String.valueOf(line.charAt(i)).equals(" ")) {
				break;
			}
			eastingString += String.valueOf(line.charAt(i));
		}
		double easting = Double.valueOf(eastingString);

		String northingString = "";
		for (int i = 0; i < line.length(); i++) {
			if (!(String.valueOf(line.charAt(lineCounter)).equals(" "))) {
				break;
			}
			lineCounter++;
		}
		for (int i = 0; i < line.length(); i++) {
			if (String.valueOf(line.charAt(lineCounter)).equals(" ")) {
				break;
			}
			northingString += String.valueOf(line.charAt(lineCounter));
			lineCounter++;
		}
		double northing = Double.valueOf(northingString);

		String heightString = "";
		for (int i = 0; i < line.length(); i++) {
			if (!(String.valueOf(line.charAt(lineCounter)).equals(" "))) {
				break;
			}
			lineCounter++;
		}
		for (int i = 0; i < line.length(); i++) {
			if (String.valueOf(line.charAt(lineCounter)).equals(" ")) {
				break;
			} else if (String.valueOf(line.charAt(lineCounter)).equals(",")) {
				heightString += ".";
			} else {
				heightString += String.valueOf(line.charAt(lineCounter));
			}

			lineCounter++;
		}
		double height = Double.valueOf(heightString);

		return new CloudPoint(easting, northing, height);
	}

	public void addListener(IMyListener l) {
		if (l != null) {
			listeners.add(l);
		}
	}

	public void fireListChanged(String string) {
		for (IMyListener l : listeners) {
			l.progressChanged(string);
		}
	}

}
